ROP攻击缓解新思路——减少ROP Gadgets的数量

在今天的玄武实验室的安全推送中,看到了Removing ROP Gadgets from OpenBSD这个议题的PPT,一开始看了下标题,感觉有点疑惑,但是没马上看,后来下午抽实践看了看,感觉这个操作还是可以的。

注意下面针对的是系统是OpenBSD,而且是kernel,思路值得借鉴

这个我之前没看到过,所以把它叫做新思路

ROP简介

说到ROP就得说说ROP Gadgets

ROP Gadgets就是汇编代码中的片段,一般是ret或者jmp结尾的

他们可能完成下面的一些功能:
1、寄存器赋值
2、给寄存器加上一个数
3、寄存器置0
4、调用函数
5、改变esp的指向
6、。。。。。。

ROP Gadgets还可以分为对齐的,还有不对齐的,不对其就是地址偏移了

比如下面的

1
8a 5d c3 	movb -61(%rbp), %bl

但是假如你将汇编解析的起始地址指向5d的位置,那么汇编的意思就变了

1
2
5d	 popq %rbp
c3 retq

比如你想执行

1
execve(“/bin//sh”, NULL, NULL)

你可能需要布置下面的ROP链

目的就是将寄存器赋值为相应的值,进行系统调用

ROP Gadgets查找工具有ROPGadget、ropper等

作者使用ROPGadget去生成直接可利用的ROP链

enter description here

如何减少ROP Gadgets

作者讲了两个思路:
1、编译出非预期的returns(就是不是我们经常看到的pop pop ret)
2、使正常的returns难以构成ROP链

并不需要使ROP Gadgets的数量变为0,只需要减少ROP Gadgets的数量使得构建一个可用的ROP链变得困难或者不可能(我们可以用上面的ROP Gadgets查找工具来衡量效果)

Polymorphic Gadget的减少

Polymorphic Gadget 中文直接翻译叫多态Gadget

看了下作者的例子就是通过地址偏移来获得Gadget

在x86/amd64有四种ret类型

抓主要矛盾:C3 ret是最常见的,也是最容易用在Gadget上的

从两方面减少polymorphic gadgets

1、寄存器的选择
2、代码的生成

寄存器的选择

常见的带c3结尾的gadgets,ret前面的汇编指令的ModR/M字节(汇编指令中,Opcode之后就是ModR/M)经常使用的寄存器如下:(这里说的比如常见的汇编:mov ebx,eax)

  • 源寄存器使用RAX/EAX/AX/AL
  • 目的寄存器使用RBX/EBX/BX/BL

此外下面的指令也经常操作RBX / EBX / BX / BL,比如inc, dec, test

而带B系列的寄存器代理很多c3字节

所以一个idea就是避免使用RBX/EBX/BX/BL

Clang按此顺序分配寄存器:RAX, RCX, RDX, RSI, RDI, R8, R9, R10, R11, RBX, R14,
R15, R12, R13, RBP

可以将RBX寄存器几乎挪到最后:RAX, RCX, RDX, RSI, RDI, R8, R9, R10, R11, R14, R15,
R12, R13, RBX, RBP

当然ebx的顺序也是要改变的

这样,性能的损耗为0,代码的字节可以忽略不计(因为有一些REX prefix字节)

最终减少了kernel的大概4500个唯一gadgets(约为6%),效果还是有的

代码的生成

我们知道有哪些指令会有return的字节(比如c3)
1、ModR/M, SIB或者特殊的指令
2、还有就是常量包含了return字节

我们可以实现相同的功能,但是不使用rerun字节或者要求强制对齐

对于ModR/M, SIB会出现return字节的如下

减少的方法就是
1、先交换寄存器
2、用寄存器进行操作
3、再交换回来

例子如下:

如果上面的方法不能使用,我们就要使用强制对齐,比如我们可以在指令前插入一个陷阱来减少gadget

  • 正常的程序会跳过我们的陷阱
  • return字节前面的int3会使得gadget受限

例子如下:

损耗总结:
1、效率损耗约为1%,因为xchg指令很快
2、代码方面,影响较小,多了6个字节,一对xchg指令
3、用来强制对齐的字节在4-11个
4、总的来说增大了kernel的大小约为2.5%

最终减少了kernel约60%的gadgets

但是我们还有一些可做
1、清理一些汇编函数
2、一些常量可能需要转换
3、重定向地址

对齐的gadget的减少(Aligned Gadget Reduction)

就是没有进行地址偏移的gadget

首先介绍下RETGUARD(小写好看一点retguard,这个其实跟windows和linux的GS/CANARY的是一样的)

实现如下:
1、给每个函数分配一个随机的cookie(用openbsd.randomdata section来分配)
2、函数开始处:计算cookie^return address,放在栈帧上,记为saved value

3、在函数返回时,计算saved value^return address,再跟cookie比较,不相等就终止程序运行

值得注意的是在返回前加了je还有int 3指令,这才是减少gadgets的功臣

因为你要把这当做gadget,你必须跳过int 3,再往前就是必须满足cookie的比较,而cookie无法预测,那就没法用了啊

方法小结:
1、损耗:运行时间多了约2%,还有就是初始化cookie的时间是可变的(跟函数的数量有关)
2、代码方面:每个函数多了31个byte,而kernel大约大了7%

最终减少了50%的gadget,15-25%的唯一gadget

针对于Arm64

arm64是有固定的指令长度,所以没有不对齐的gadget,只有对齐的gadget

对于对齐gadget的减少同样也可以是上面int 3的思路,只不过arm64是brk #0x1

这样就几乎删除绝大多数gadget了

在6.3-release arm64 kernel中ROP gadgets的数量:69935
在6.4-release arm64 kernel中ROP gadgets的数量:46

而剩余的gadget是在引导代码中的汇编中,具体如下:

  • create_pagetables
  • link_l0_pagetable
  • link_l1_pagetable
  • build_l1_block_pagetable
  • build_l2_block_pagetable

引导后OpenBSD可以unlink或粉碎引导代码,那么这些功能在系统运行时就不可用

那么在用户层,那么gadget也几乎为0了,可能存在于crt0,ld.so

最后看看效果图,先看统计的

看看常用的库还有sshd服务

最后看看ROPGadget查找效果,已经不能自动化构成利用链了

这是6.4的libc

6.5的更加惨不忍睹

结尾

还有更多东西可做

比如重定向地址,剩余的可用于gadget的汇编,JOP还没有动

参考

AsiaBSDCon 2019 —— Removing ROP Gadgets from OpenBSD

https://www.openbsd.org/papers/asiabsdcon2019-rop-slides.pdf

打赏专区